import os
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from docx import Document
import threading
import time
import sys
import win32com.client
import pythoncom

class DocReplacer:
    def __init__(self, root):
        self.root = root
        self.root.title("文档文本替换工具 (保留格式)")
        self.root.geometry("800x600")
        self.root.resizable(True, True)

        # 创建主框架
        self.main_frame = tk.Frame(self.root, padx=10, pady=10)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建选择文件夹部分
        self.folder_frame = tk.LabelFrame(self.main_frame, text="选择文件夹", padx=5, pady=5)
        self.folder_frame.pack(fill=tk.X, pady=5)

        self.folder_path = tk.StringVar()
        self.folder_entry = tk.Entry(self.folder_frame, textvariable=self.folder_path, width=70)
        self.folder_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

        self.browse_button = tk.Button(self.folder_frame, text="浏览", command=self.browse_folder)
        self.browse_button.pack(side=tk.RIGHT, padx=5)

        # 创建替换文本部分
        self.replace_frame = tk.LabelFrame(self.main_frame, text="替换文本", padx=5, pady=5)
        self.replace_frame.pack(fill=tk.X, pady=5)

        tk.Label(self.replace_frame, text="查找文本:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.find_text = tk.Entry(self.replace_frame, width=50)
        self.find_text.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W+tk.E)

        tk.Label(self.replace_frame, text="替换为:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        self.replace_text = tk.Entry(self.replace_frame, width=50)
        self.replace_text.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W+tk.E)

        self.replace_frame.columnconfigure(1, weight=1)

        # 创建操作按钮部分
        self.button_frame = tk.Frame(self.main_frame)
        self.button_frame.pack(fill=tk.X, pady=5)

        self.replace_button = tk.Button(self.button_frame, text="开始替换", command=self.start_replace)
        self.replace_button.pack(side=tk.RIGHT, padx=5)

        # 创建进度显示部分
        self.progress_frame = tk.LabelFrame(self.main_frame, text="处理进度", padx=5, pady=5)
        self.progress_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.progress_text = scrolledtext.ScrolledText(self.progress_frame, wrap=tk.WORD, width=80, height=20)
        self.progress_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        self.status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

        # 处理状态
        self.processing = False

    def browse_folder(self):
        folder_selected = filedialog.askdirectory()
        if folder_selected:
            self.folder_path.set(folder_selected)

    def log(self, message):
        self.progress_text.insert(tk.END, message + "\n")
        self.progress_text.see(tk.END)
        self.root.update_idletasks()

    def start_replace(self):
        if self.processing:
            messagebox.showwarning("警告", "正在处理中，请等待完成")
            return

        folder_path = self.folder_path.get()
        find_text = self.find_text.get()
        replace_text = self.replace_text.get()

        if not folder_path or not os.path.isdir(folder_path):
            messagebox.showerror("错误", "请选择有效的文件夹")
            return

        if not find_text:
            messagebox.showerror("错误", "请输入要查找的文本")
            return

        # 清空进度显示
        self.progress_text.delete(1.0, tk.END)

        # 在新线程中执行替换操作
        self.processing = True
        self.status_var.set("处理中...")
        threading.Thread(target=self.replace_in_docs, args=(folder_path, find_text, replace_text), daemon=True).start()

    def replace_in_docx(self, file_path, find_text, replace_text):
        """处理.docx格式文件，保留文本格式"""
        try:
            # 打开文档
            doc = Document(file_path)
            replaced = False

            # 替换段落中的文本
            for para in doc.paragraphs:
                if find_text in para.text:
                    # 在run级别替换文本以保留格式
                    self.replace_text_in_runs(para.runs, find_text, replace_text)
                    replaced = True

            # 替换表格中的文本
            for table in doc.tables:
                for row in table.rows:
                    for cell in row.cells:
                        for paragraph in cell.paragraphs:
                            if find_text in paragraph.text:
                                self.replace_text_in_runs(paragraph.runs, find_text, replace_text)
                                replaced = True

            # 如果有替换，保存文档
            if replaced:
                doc.save(file_path)
                self.log(f"  - 已替换并保存")
                return True
            else:
                self.log(f"  - 未找到匹配文本")
                return False
        except Exception as e:
            self.log(f"  - 处理.docx文件失败: {str(e)}")
            return False

    def replace_text_in_runs(self, runs, find_text, replace_text):
        """在保留格式的情况下替换runs中的文本"""
        if not runs:
            return

        # 获取所有runs的文本
        text = "".join([run.text for run in runs])

        # 如果没有找到要替换的文本，直接返回
        if find_text not in text:
            return

        # 处理文本替换
        # 这种方法处理跨多个run的文本替换
        start_idx = 0
        runs_text = []
        runs_mapping = []

        # 构建runs的映射关系
        for i, run in enumerate(runs):
            if not run.text:
                continue

            end_idx = start_idx + len(run.text)
            runs_text.append(run.text)
            runs_mapping.append((start_idx, end_idx, i))
            start_idx = end_idx

        full_text = "".join(runs_text)
        new_text = full_text.replace(find_text, replace_text)

        # 如果文本没有变化，直接返回
        if full_text == new_text:
            return

        # 重新分配文本到各个run，保留格式
        current_position = 0
        for start_idx, end_idx, run_idx in runs_mapping:
            run_length = end_idx - start_idx
            if current_position + run_length <= len(new_text):
                runs[run_idx].text = new_text[current_position:current_position + run_length]
            else:
                # 处理最后一个run可能超出新文本长度的情况
                if current_position < len(new_text):
                    runs[run_idx].text = new_text[current_position:]
                else:
                    runs[run_idx].text = ""
            current_position += run_length

    def replace_in_doc(self, file_path, find_text, replace_text):
        """处理.doc格式文件，保留文本格式"""
        try:
            # 初始化COM
            pythoncom.CoInitialize()

            # 创建Word应用程序对象
            word = win32com.client.Dispatch("Word.Application")
            word.Visible = False

            try:
                # 打开文档
                doc = word.Documents.Open(file_path)

                # 执行查找和替换
                find_obj = word.Selection.Find
                find_obj.ClearFormatting()
                find_obj.Text = find_text
                # 不清除替换文本的格式，这样可以保留原文本的格式
                # find_obj.Replacement.ClearFormatting()  # 注释掉这一行
                find_obj.Replacement.Text = replace_text

                # 设置格式保留选项
                # 0 = wdReplaceNone, 1 = wdReplaceOne, 2 = wdReplaceAll
                # 4 = wdPreserveFormatting (保留格式)
                wdReplaceAll = 2
                wdPreserveFormatting = 16  # Word常量，表示保留格式

                # 执行替换
                replaced = False
                if find_obj.Execute(FindText=find_text, ReplaceWith=replace_text,
                                   Forward=True, Wrap=1, MatchCase=False,
                                   MatchWholeWord=False, MatchWildcards=False,
                                   MatchSoundsLike=False, MatchAllWordForms=False,
                                   Format=True, Replace=wdReplaceAll):
                    replaced = True

                # 保存并关闭文档
                doc.Save()
                doc.Close()

                if replaced:
                    self.log(f"  - 已替换并保存")
                    return True
                else:
                    self.log(f"  - 未找到匹配文本")
                    return False
            except Exception as e:
                print(e)
            finally:
                # 退出Word应用程序
                word.Quit()
                # 释放COM资源
                pythoncom.CoUninitialize()
        except Exception as e:
            self.log(f"  - 处理.doc文件失败: {str(e)}")
            return False

    def replace_in_docs(self, folder_path, find_text, replace_text):
        try:
            start_time = time.time()
            self.log(f"开始处理文件夹: {folder_path}")
            self.log(f"查找文本: '{find_text}'")
            self.log(f"替换为: '{replace_text}'")
            self.log("----------------------------")

            total_files = 0
            processed_files = 0
            replaced_files = 0

            # 遍历文件夹
            for root, dirs, files in os.walk(folder_path):
                for file in files:
                    if file.lower().endswith(('.doc', '.docx')):
                        total_files += 1
                        file_path = os.path.join(root, file)
                        self.log(f"处理文件: {file_path}")

                        replaced = False
                        if file.lower().endswith('.docx'):
                            replaced = self.replace_in_docx(file_path, find_text, replace_text)
                        elif file.lower().endswith('.doc'):
                            replaced = self.replace_in_doc(file_path, find_text, replace_text)

                        if replaced:
                            replaced_files += 1

                        processed_files += 1

            elapsed_time = time.time() - start_time
            self.log("----------------------------")
            self.log(f"处理完成! 耗时: {elapsed_time:.2f}秒")
            self.log(f"总文件数: {total_files}")
            self.log(f"处理文件数: {processed_files}")
            self.log(f"替换文件数: {replaced_files}")

            self.status_var.set(f"完成 - 处理了 {processed_files} 个文件，替换了 {replaced_files} 个文件")
        except Exception as e:
            self.log(f"发生错误: {str(e)}")
            self.status_var.set("处理出错")
        finally:
            self.processing = False

def main():
    try:
        root = tk.Tk()
        app = DocReplacer(root)
        root.mainloop()
    except Exception as e:
        import traceback
        error_msg = f"发生错误: {str(e)}\n{traceback.format_exc()}"
        print(error_msg)

        # 尝试将错误写入日志文件
        try:
            with open("replacer_error.log", "w") as f:
                f.write(error_msg)
        except:
            pass

        # 如果GUI已经创建，显示错误消息框
        try:
            messagebox.showerror("错误", f"程序启动失败: {str(e)}\n详细信息已写入replacer_error.log文件")
        except:
            pass

if __name__ == "__main__":
    main()

